Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added findaxis, finddim, and hasdim #89

Closed
wants to merge 3 commits into from
Closed

Added findaxis, finddim, and hasdim #89

wants to merge 3 commits into from

Conversation

Tokazama
Copy link
Contributor

@Tokazama Tokazama commented Sep 6, 2019

These are zero allocation methods that should allow easy development of methods based on the axis names. For example, defining timedim with this implementation would only require timedim(x) = finddim(x, :time).

These are zero allocation methods that should allow easy development of
axis name based methods.
@codecov
Copy link

codecov bot commented Sep 6, 2019

Codecov Report

Merging #89 into master will increase coverage by 0.94%.
The diff coverage is 93.75%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #89      +/-   ##
==========================================
+ Coverage    67.6%   68.55%   +0.94%     
==========================================
  Files           9        9              
  Lines         426      442      +16     
==========================================
+ Hits          288      303      +15     
- Misses        138      139       +1
Impacted Files Coverage Δ
src/ImageCore.jl 61.11% <ø> (ø) ⬆️
src/traits.jl 86.59% <93.75%> (+1.41%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6e8a382...6d273bd. Read the comment docs.

@codecov
Copy link

codecov bot commented Sep 6, 2019

Codecov Report

Merging #89 into master will increase coverage by 1.65%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #89      +/-   ##
==========================================
+ Coverage    67.6%   69.26%   +1.65%     
==========================================
  Files           9        9              
  Lines         426      449      +23     
==========================================
+ Hits          288      311      +23     
  Misses        138      138
Impacted Files Coverage Δ
src/ImageCore.jl 61.11% <ø> (ø) ⬆️
src/traits.jl 88.46% <100%> (+3.27%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6e8a382...06ea089. Read the comment docs.

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally approving but you'll see there are some design issues to think about.

src/traits.jl Outdated
if hasdim_noerror(syms, name)
return getfield(nt, name)
else
return 0:0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0:0 is not empty (it has one item). I think you mean 0:-1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's is what I was going for but if there's something more rational to fallback on I'm all ears.

src/traits.jl Outdated
Returns the axis (as in `axes(img, dim)`) that corresponds to the `name`. If no
matching name is found any empy axis (i.e. `0:0`) is returned.
"""
@inline findaxis(A::T, name::Symbol) where {T} = _findaxis(namedaxes(A), name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

::T where T is extraneous here.

src/traits.jl Outdated
_hasdim(::HasDimNames{true}, x::T, name::Symbol) where {T} = hasdim_noerror(names(x), name)
_hasdim(::HasDimNames{false}, x::T, name::Symbol) where {T} = false

Base.@pure function hasdim_noerror(dimnames::Tuple{Vararg{Symbol, N}}, name::Symbol) where N
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name here is slightly odd. You just want it to be different from hasdim I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adapted the method from what I found in NamedDims and kind of stuck with the naming. My goal was to isolate it so it didn't have any other generic methods. I'm never sure if I'm going to mess up use of @pure because I've read very different explanations of how it should be used from experienced users. I'd be happy to make it consistent with the other methods if it's not a problem.

src/traits.jl Outdated
"""
hasdim(x::T, name::Symbol) where {T} = _hasdim(HasDimNames(T), x, name)
_hasdim(::HasDimNames{true}, x::T, name::Symbol) where {T} = hasdim_noerror(names(x), name)
_hasdim(::HasDimNames{false}, x::T, name::Symbol) where {T} = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finddim(A, :dim_1) could work even if !hasdim(A, :dim_1). I worry a bit about API confusion/consistency in such cases. How important is it to have default names? Should we eliminate them and support operations only on explicitly-named axes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. That needs to be more consistent. It seems kind of pointless to have it act on a default name that has no significant meaning. This makes me think that if there are default names they should probably be more meaningful, like those in AxisArrays. It may also help users if there's consistency with how AxisArrays names worked. I don't have a strong opinion either way, as long as I don't end being the guy that breaks JuliaImages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'm a fan of :row, :col, and :page either. Those don't sound like image properties. At best they are linear algebra properties; just because we use 2d arrays for both doesn't mean image == matrix. (How often do you multiply an image times a vector??)

Can we just drop the default names altogether, and have these only operate on arrays with explicitly-assigned names?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vote for :dim_1, :dim_2, and :dim_3

* New (old AxisArray) default names
* More logical internal names (e.g., `_hasdim`)
1. The internal names now follow the general rule that the first internal
layer of a method is "_methodname" and the second is "__methodname".
This makes the internal vs public api more specific.

2. Added tests for each default name. Hopefully this gives 100% coverage
for the trait interfaces methods.
Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's possible to just get rid of the default names, I think we can merge this. If it's necessary to keep them, please elaborate on an actual use-case for which this is a make-or-break deal.

function namedaxes(::HasDimNames{false}, img::AbstractArray{T,N}) where {T,N}
NamedTuple{default_names(Val(N))}(axes(img))
Returns the axis (as in `axes(img, dim)`) that corresponds to the `name`. If no
matching name is found any empy axis (i.e. `0:0`) is returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
matching name is found any empy axis (i.e. `0:0`) is returned.
matching name is found any empty axis (i.e. `0:-1`) is returned.

I almost wonder if axisrange is a better name for this operation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case would you be thinking of the axis as the names and range as the tuple of axes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just trying to ask myself, "what would I expect findaxis to do?" And I am not sure. findmin returns both the value and location of the minimum. So maybe findaxis should return the value (i.e., range) and dimension index (like finddim)? Or maybe axisvalue, which is again back to the names chosen by AxisArrays.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm trying to imitate the behavior of axes(img, i) where i would be a symbol. The trick is that whatever type img is may not wrap axes that are also directly linked to their names (e.g., one array type stores names wraps another type that stores axes). So if we can access the axes and names independently then we can use namedaxes to connect these otherwise independent features.

I think the rationale of findaxis returning something similar to findmin makes a lot of sense though. In this case maybe findaxis would return something like NamedTuple{(:dim_1)}((1:10,)) and axisvalue would return the individual axis/range.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could go with that latter proposal.

@timholy
Copy link
Member

timholy commented Sep 16, 2019

I know that default_names has already made it in, but since there are no users in JuliaImages I think it's fine to take it out again if we need to.

One suggestion: back when JuliaImages was mostly a one-person show, I was writing the algorithms at the same time as deciding about traits, and the efforts of implementing the algorithms forced me to think clearly about what I actually needed. (Not that I always got the design right, mind you!) You may be doing the same thing, so when you encounter an example, don't hesitate to post it in a comment or gist. It would help make sure we are thinking about the same thing.

@Tokazama
Copy link
Contributor Author

The downside to not using defaults_names is that any method that uses these
traits would have to account for a different type. With the current
implementation most of ImageAxes traits could be written as:

colordim(img) = finddim(img, :color)
coloraxis(img) = findaxis(img, :color)

timedim(img) = finddim(img, :time)
timeaxis(img) = findaxis(img, :time)

Without defaults it would either throw an error or the default behavior would
have to be described at the level of the method.

@inline finddim(A::T, name::Symbol) where {T} = _finddim(HasDimNames(T), A, name)
_finddim(::HasDimNames{false}, A::T, name::Symbol) where {T} = nothing
_finddim(::HasDimNames{true}, A::T, name::Symbol) where {T} = __finddim(names(A), name)

function timedim(img)
    d = finddim(img)
    if isnothing(d)
        return 0
    else
        return d
    end
end

I don't think there's anything wrong with this approach, it's just more verbose.

@timholy
Copy link
Member

timholy commented Sep 16, 2019

I kind of like the verbosity. And why couldn't timedim(img) = finddim(img, :time)? Why is 0 better than nothing?

@Tokazama
Copy link
Contributor Author

The main reason was so that it always returned something of the same type, but that's only an issue if img is inferred to be a union of types that can return HasDimNames{true} or HasDimNames{false}. So the only situation where it would be useful is something like:

d = (NamedDimsArray, Array)

timedim(d[1])

@Tokazama
Copy link
Contributor Author

Actually, returning nothing might be a nice way of allowing methods like timedim to more easily implement custom defaults or errors when HasDimNames{false} without throwing an error. That's hard to do if everything is Int no matter what

@Tokazama
Copy link
Contributor Author

It looks like the main methods this PR was for (findaxis, finddim, and hasdim) will be handled by whatever ends up being the backend for our arrays (maybe NamedDims). Would it make sense to close this PR and move efforts there.

@Tokazama
Copy link
Contributor Author

Unless there are any objections, I'm going to close this and probably open a more disruptive but complete PR using more recent developments for axes and named dimensions.

@Tokazama Tokazama closed this Apr 24, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants